<?php
/**
 * CryptographyUtil class used to hash and encode password to store in database.
 *
 * Also it contains functions to decode an given hashed password. This class will be mainly
 * used in user authentication process. This class only provide functions to do cryptogra and
 * reversing cryptogra process, this class will not interact with database in any way.
 *
 * Usage:
 * To encrypt a string or password with default algorithm which is sha1:
 *   $encryptedPassword = CryptographyUtil::encryptPassword('password');
 * To encrypt a string or password with different algorithm like md5:
 *   $encryptedPassword = CryptographyUtil::encryptPassword('password', 'md5');
 *
 */
class CryptographyUtil {

	/**
	 * All avaiable algorithms this class can hash.
	 *
	 * @var array
	 */
	private static $allAlgorithms = array('sha1','md5');

	/**
	 * Prefix of final encoding process.
	 *
	 * @var string
	 */
	private static $hashPrefix;

	/**
	 * Salt for hashing text.
	 *
	 * @var string
	 */
	private static $salt;

	/**
	 * Password to be hashed and encrypted.
	 *
	 * @var string
	 */
	private static $password;

	/**
	 * Name of selected hashing algorithm.
	 *
	 * i.e: "md5", "sha256", "haval160,4"
	 * Default value is sha1.
	 *
	 * @var string
	 * @link http://docs.php.net/manual/en/function.hash.php
	 */
	private static $defaultAlgorithm = 'md5';

	/**
	 * Final output of the whole process.
	 *
	 * @var string
	 */
	private static $output;

	/**
	 * Used to generate a random value as salt for later usage.
	 *
	 * It will generate an random value first then hash the value, finally get 8
	 * characters of the hashed value and set it to salt variable.
	 *
	 * @access private
	 * @since 1.0
	 * @return void
	 */
	private static function randomSalt() {
		$random		 = uniqid(rand(), true);
		$hashedValue = self::doHash($random);
		self::setSalt(substr($hashedValue, 0, 8));
	}

	/**
	 * Hash password with current salt.
	 *
	 * @access private
	 * @since 1.0
	 * @return void
	 */
	private static function hashPasswordWithSalt() {
		self::setOutput(self::doHash(self::$salt.self::$password, true));
	}

	/**
	 * Encode password with salt and hashed value.
	 *
	 * @access private
	 * @since 1.0
	 * @return void
	 */
	private static function encodePassword() {
		self::setOutput(base64_encode(self::$salt.self::$output));
	}

	/**
	 * Decode password value.
	 *
	 * Most likely the password will be retrived from database and it is a
	 * hashed password.
	 *
	 * @access private
	 * @since 1.0
	 * @return void
	 */
	private static function decodePassword() {
		self::setOutput(base64_decode(self::$password));
		self::setSalt(substr(self::$output, 0, 8));
	}

	/**
	 * Remove the prefix for password to get real hashed password.
	 *
	 * @access private
	 * @since 1.0
	 * @return void
	 */
	private static function hashedPassword() {
		self::setOutput(str_replace(self::$salt, '', self::$output));
	}

	/**
	 * This function is used for hashing a string.
	 *
	 * The hashing method will be different depend on value of $defaultAlgorithm.
	 * Types of algorithms will be pre-defined.
	 *
	 * @param string  $text		  Text needed to be hashed.
	 * @param boolean $raw_output When set to TRUE, outputs raw binary data. FALSE outputs lowercase hexits.
	 *
	 * @access private
	 * @since 1.0
	 * @return string|null		  If it is an valided algorithm then return the hashed string, otherwise return null.
	 */
	private static function doHash($text,$raw_output = false) {
		if (self::checkAlgorithm()) {
			switch (strtolower(self::getDefaultAlgorithm())) {
				case 'sha1':
					return sha1($text, $raw_output);
				case 'md5':
					return md5($text, $raw_output);
				default:
					return null;
			}
		}

		return null;
	}

	/**
	 * Used to check if default algorithm is in the algorithm list.
	 *
	 * If it is not then the default algorithm is not a valid algorithm.
	 *
	 * @access private
	 * @since 1.0
	 * @return boolean Whether the default algorithm is an valid algorithm.
	 */
	private static function checkAlgorithm(){
		if (in_array(self::getDefaultAlgorithm(), self::getAllAlgorithms())) {
			return true;
		}

		return false;
	}

	/**
	 * Process for the whole decoding for password.
	 *
	 * This is a reverse process of function encryptPassword().
	 * This function will manipulate the output to be an hashed string.
	 *
	 * @param string $password  Password needed to be decrypted.
	 * @param string $algorithm Algorithm needed to be used for decryption process. Default value is sha1.
	 *
	 * @access private
	 * @since 1.0
	 * @return string An hashed string which was decrypted.
	 */
	private static function decryptPassword($password = '',$algorithm = 'md5') {
		self::init($password,$algorithm);
		self::decodePassword();
		self::hashedPassword();
		return self::getOutput();
	}

	/**
	 * Getter method for current salt.
	 *
	 * @access private
	 * @since 1.0
	 * @return string Current salt.
	 */
	private static function getSalt() {
		return self::$salt;
	}

	/**
	 * Setter method for current salt.
	 *
	 * @param string $salt Current salt.
	 *
	 * @access private
	 * @since 1.0
	 * @return void
	 */
	private static function setSalt($salt) {
		self::$salt = $salt;
	}

	/**
	 * Getter method for current password.
	 *
	 * @access private
	 * @since 1.0
	 * @return string Current password.
	 */
	private static function getPassword() {
		return self::$password;
	}

	/**
	 * Setter method for current password.
	 *
	 * @param string $password Current password.
	 *
	 * @access private
	 * @since 1.0
	 * @return void
	 */
	private static function setPassword($password) {
		self::$password = $password;
	}

	/**
	 * Getter method for current output.
	 *
	 * @access private
	 * @since 1.0
	 * @return string Current output.
	 */
	private static function getOutput() {
		return self::$output;
	}

	/**
	 * Setter method for current output.
	 *
	 * @param string $output Current output.
	 *
	 * @access private
	 * @since 1.0
	 * @return void
	 */
	private static function setOutput($output) {
		self::$output = $output;
	}

	/**
	 * Getter method for current algorithm.
	 *
	 * @access private
	 * @since 1.0
	 * @return string Current algorithm.
	 */
	private static function getDefaultAlgorithm() {
		return self::$defaultAlgorithm;
	}

	/**
	 * Setter method for current algorithm.
	 *
	 * The specified algorithm must be in allAlgorithms.
	 * @see getAllAlgorithms
	 *
	 * @param string $algorithm Current algorithm.
	 *
	 * @access private
	 * @since 1.0
	 * @return void
	 */
	private static function setDefaultAlgorithm($algorithm) {
		if (in_array($algorithm, self::getAllAlgorithms())) {
			self::$defaultAlgorithm = strtolower(trim($algorithm));
			self::setHashPrefix();
		}
	}

	/**
	 * Getter method for current hashPrefix.
	 *
	 * @access private
	 * @since 1.0
	 * @return string Current hashPrefix.
	 */
	private static function getHashPrefix() {
		return self::$hashPrefix;
	}

	/**
	 * Setter method for current hashPrefix.
	 *
	 * Hash prefix should be an string with 3 chars and followed by an ":".
	 * The 3 chars string will be generated based on current algorithm.
	 * It used to hide the hash algorithm in a hashed password string.
	 *
	 * @access private
	 * @since 1.0
	 * @return void
	 */
	private static function setHashPrefix() {
		if (self::getDefaultAlgorithm() != '') {
			self::$hashPrefix = substr(self::doHash(self::getDefaultAlgorithm()), 0,3).':';
		}
	}

	/**
	 * Setter method for $allAlgorithms.
	 *
	 * @param array $allAlgorithms Avaiable hashing algorithms.
	 *
	 * @access private
	 * @since 1.0
	 * @return void
	 */
	private static function setAllAlgorithms($allAlgorithms){
		self::$allAlgorithms = $allAlgorithms;
	}

	/**
	 * Getter method for $allAlgorithms.
	 *
	 * @since 1.0
	 * @return array Avaiable hashing algorithms.
	 */
	public static function getAllAlgorithms() {
		return self::$allAlgorithms;
	}

	/**
	 * Process the whole encryption process for an given password and algorithm.
	 *
	 * @param string $password  Password needed to be encrypted.
	 * @param string $algorithm Algorithm needed to be used for decryption process. Default value is sha1.
	 *
	 * @since 1.0
	 * @return string The output variable which has been encrypted.
	 */
	public static function encryptPassword($password = '',$algorithm = 'md5') {
		self::init($password,$algorithm);
		self::randomSalt();
		self::hashPasswordWithSalt();
		self::encodePassword();

		return self::getOutput();
	}

	/**
	 * This function will do initialization for $password and $algorithm.
	 *
	 * @param string $password  Password needed to be processed.
	 * @param string $algorithm Algorithm needed to be used to process password.
	 *
	 * @since 1.0
	 * @return void
	 */
	public static function init($password = '', $algorithm = 'md5') {
		self::setOutput('');
		self::setSalt('');
		self::setPassword($password);
		self::setDefaultAlgorithm($algorithm);
	}

	/**
	 * This function is used to check if plain input string match the encrypted string.
	 *
	 * @param string $stringAttempt   Input string for instance it could be input password.
	 * @param string $encryptedString Encrypted string.
	 * @param string $algorithm       Algorithm used for checking equals. Default value is sha1.
	 *
	 * @since 1.0
	 * @return boolean True means two inputs are match, false means not match.
	 */
	public static function checkEquals($stringAttempt, $encryptedString, $algorithm = 'md5') {
		$decryptedString = CryptographyUtil::decryptPassword($encryptedString,$algorithm);
		$hashedString = CryptographyUtil::doHash(CryptographyUtil::getSalt().$stringAttempt, true);

		return ($hashedString == $decryptedString);
	}
}
